package com.msimple.order.domain.model;

import com.msimple.order.infrastructure.exception.OrderCannotBeModifiedException;
import com.msimple.order.infrastructure.exception.PaidPriceNotSameWithOrderPriceException;
import com.msimple.order.infrastructure.exception.ProductNotInOrderException;
import com.msimple.sdk.event.order.OrderAddressChangedEvent;
import com.msimple.sdk.event.order.OrderCreatedEvent;
import com.msimple.sdk.event.order.OrderPaidEvent;
import com.msimple.sdk.event.order.OrderProductChangedEvent;
import com.msimple.sdk.representation.order.OrderRepresentation;
import com.msimple.sdk.representation.order.OrderSummaryRepresentation;
import com.msimple.sdk.representation.order.OrderItem;
import com.msimple.shared.model.Address;
import com.msimple.shared.model.BaseAggregate;
import lombok.Builder;
import lombok.Getter;

import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.stream.Collectors;

import static java.math.BigDecimal.ZERO;
import static java.time.Instant.now;


/**
 *  Order aggregate
 * @Title:      Order
 * @Package:    Order
 * @Author:     M.simple
 * @Remark:     The modified content
 * @CreateDate: 2019-10-13 16:10
 * @Version:    v2.0
 */
@Getter
@Builder
public class Order extends BaseAggregate {
    
    private String id;

    private List<OrderProductItem> items;

    private BigDecimal totalPrice;

    private OrderStatus status;

    private Address address;
    
    private Instant createdAt;
    
    /**
     * order create logic
     * @method      create
     * @param       id:item:address
     * @return      
     * @author      M.simple
     * @date        2019-10-13 16:11
     * @version     v2.0
     */
    public static Order create(String id, List<OrderProductItem> items, Address address) {
        Order order = Order.builder()
                .id(id)
                .items(items)
                .totalPrice(calculateTotalPrice(items))
                .status(OrderStatus.CREATED)
                .address(address)
                .createdAt(now())
                .build();
        order.raiseCreatedEvent(id, items, address);
        return order;
    }

    /**
     * calculate order price
     * @method      calculateTotalPrice
     * @param       items:
     * @return      
     * @author      M.simple
     * @date        2019-10-13 16:11
     * @version     v2.0
     */
    private static BigDecimal calculateTotalPrice(List<OrderProductItem> items) {
        return items.stream()
                .map(OrderProductItem::totalPrice)
                .reduce(ZERO, BigDecimal::add);
    }

    /**
     * registe domain event (create event)
     * @method      raiseCreatedEvent
     * @param       id:items:address
     * @return      
     * @author      M.simple
     * @date        2019-10-13 16:12
     * @version     v2.0
     */
    private void raiseCreatedEvent(String id, List<OrderProductItem> items, Address address) {
        List<com.msimple.sdk.event.order.OrderItem> orderItems = items.stream()
                .map(orderProductItem -> new com.msimple.sdk.event.order.OrderItem(orderProductItem.getProductId(),
                        orderProductItem.getCount())).collect(Collectors.toList());
        
        addEvent(new OrderCreatedEvent(id, totalPrice, address, orderItems, createdAt));
    }

    /**
     * change product count
     * @method      changeProductCount
     * @param       productId:count
     * @return      
     * @author      M.simple
     * @date        2019-10-13 16:13
     * @version     v2.0
     */
    public void changeProductCount(String productId, int count) {
        if (this.status == OrderStatus.PAID) {
            throw new OrderCannotBeModifiedException(this.id);
        }

        OrderProductItem orderProductItem = retrieveItem(productId);
        int originalCount = orderProductItem.getCount();
        orderProductItem.updateCount(count);
        this.totalPrice = calculateTotalPrice(items);
        addEvent(new OrderProductChangedEvent(id, productId, originalCount, count));
    }

    /**
     * filter product items
     * @method      retrieveItem
     * @param       productId:
     * @return      
     * @author      M.simple
     * @date        2019-10-13 16:14
     * @version     v2.0
     */
    private OrderProductItem retrieveItem(String productId) {
        return items.stream()
                .filter(item -> item.getProductId().equals(productId))
                .findFirst()
                .orElseThrow(() -> new ProductNotInOrderException(productId, id));
    }

    /**
     * order payment
     * @method      pay
     * @param       paidPrice:
     * @return
     * @author      M.simple
     * @date        2019-10-9 16:15
     * @version     v2.0
     */
    public void pay(BigDecimal paidPrice) {
        if (!this.totalPrice.equals(paidPrice)) {
            throw new PaidPriceNotSameWithOrderPriceException(id);
        }
        this.status = OrderStatus.PAID;
        addEvent(new OrderPaidEvent(this.getId()));
    }

    /**
     * change order address detail
     * @method      changeAddressDetail
     * @param       detail:
     * @return      
     * @author      M.simple
     * @date        2019-10-9 16:15
     * @version     v2.0
     */
    public void changeAddressDetail(String detail) {
        if (this.status == OrderStatus.PAID) {
            throw new OrderCannotBeModifiedException(this.id);
        }

        this.address = this.address.changeDetailTo(detail);
        addEvent(new OrderAddressChangedEvent(getId(), detail, address.getDetail()));
    }

    /**
     * convert order to order Representation
     * @method      toRepresentation
     * @param        
     * @return      OrderRepresentation
     * @author      M.simple
     * @date        2019-10-9 16:15
     * @version     v2.0
     */
    public OrderRepresentation toRepresentation() {
        List<OrderItem> itemRepresentations = this.getItems().stream()
                .map(orderProductItem -> new OrderItem(orderProductItem.getProductId(),
                        orderProductItem.getCount(),
                        orderProductItem.getItemPrice()))
                .collect(Collectors.toList());

        return new OrderRepresentation(this.getId(),
                itemRepresentations,
                this.getTotalPrice(),
                this.getStatus().name(),
                this.getAddress(),
                this.getCreatedAt());
    }

    /**
     * convert order to order OrderSummaryRepresentation
     * @method      toRepresentation
     * @param
     * @return      OrderRepresentation
     * @author      M.simple
     * @date        2019-10-9 16:15
     * @version     v2.0
     */
    public OrderSummaryRepresentation toSummary() {
        return new OrderSummaryRepresentation(this.getId(),
                this.getTotalPrice(),
                this.getStatus().name(),
                this.getCreatedAt(),
                this.getAddress());
    }

}
